home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / osr5 / sco / scripts / mbill < prev    next >
Encoding:
AWK Script  |  1997-08-26  |  56.1 KB  |  1,481 lines

  1. #!/usr/local/bin/gawk -f
  2. #!/u/johnd/bin/gawk -f
  3. # @(#) mbill.gawk 3.1 96/05/06
  4. # 92/09/06 john h. dubois iii (john@armory.com)
  5. # 92/12/09 Added help.
  6. # 93/03/04 Added p option
  7. # 93/06/19 Set Err when exiting due to invalid option
  8. # 94/03/11 Cleaned up
  9. # 94/07/05 2.0 Generalized from rent processing program.
  10. # 95/04/01 Modified to pay attention to the day of the month given for rate
  11. #          change lines, to allow prorating.
  12. # 95/06/18 Print the sum of the owed values.  Added c option.
  13. # 95/11/02 Let party names be given in assorted case
  14. # 95/12/16 3.0 Added ability to specify multiple types of bills, and B option.
  15. # 95/12/18 Added le options
  16. # 96/01/12 Do not print 0 amounts for months.
  17. # 96/05/06 Search only for MBILL in environment.
  18. #
  19. # todo: let verbose output be broken down by rate type.
  20. # todo: Let certain vars be set in bill file.
  21. # todo: Generalize who amounts are owed by & owed to & how they are specified
  22. # todo: Add option to only produce output for certain users.
  23. # todo: Let decimal fractions be given in payments.  Currently gives bogus err.
  24. # Bugs:
  25. # When a future rate change occurs, it causes billing to be calculated up
  26. # through that date.  Billing should end at the current (or specified) date
  27. # regardless of future changes.
  28.  
  29. BEGIN {
  30.     Run()
  31.     exit 0
  32. }
  33.  
  34. # Globals: see Setup().
  35. function Run(  AllParties,Paid,LastPay,LastAmt,RateName,RateNames,TotalBilled) {
  36.     Setup()
  37.     # Make gawk know these are all arrays
  38.     # Every party found is made an index of AllParties; it also maps the
  39.     # lower-case version of each party name to a mixed-case version
  40.     split("",AllParties)
  41.     ProcFiles(ARGC,ARGV,Running,AllParties,Paid,LastPay,LastAmt,TotalBilled,
  42.     RateNames,MRates,MRateChanges,PrintChanges,UnnamedName,Debug,
  43.     BillLatest,BillThrough)
  44.     if (Err != "")
  45.     exit Err
  46.     # AllParties[], Paid[], LastPay[], LastAmt[], TotalBilled[],
  47.     # RateNames[], MRates[], and MRateChanges[] come from ProcFiles().
  48.     # Brief, NoBreakdown, LastPayment, and Debug come from Setup().
  49.     PrintResults(AllParties,Paid,LastPay,LastAmt,Brief,NoBreakdown,
  50.     TotalBilled,RateNames,MRates,MRateChanges,LastPayment,Debug)
  51. }
  52.  
  53. # Sets globals:
  54. # PrintChanges: Rate changes should be printed.
  55. # LastPayment: Last payments should be printed.
  56. # Debug: Debugging is on.
  57. # UnnamedName: The name to use for the unnamed bill type.
  58. # Running: Print lines as they are processed.
  59. # Brief: Print only a brief summary.
  60. # NoBreakdown: Do not print a breakdown of amounts owed.
  61. # BillLatest: Bill through last day of last month given in a file date.
  62. # BillThrough: A specific date to bill through (in epoch days).
  63. function Setup(  Name,Usage,FileVar,hrcFile,rcFile,DefFile) {
  64.     Name = "mbill"
  65.     UnnamedName = "Bills"
  66.     Usage = "Usage: " Name " [-bBchHlnpr] [-e<date>] [-u<name>] [bill-file ...]"
  67.     FileVar = "MBILL"
  68.     hrcFile = ".mbillrc"    # Name without the leading ~/, for help
  69.     rcFile = "~/" hrcFile
  70.     # f is pseudo-arg for filename
  71.     ARGC = Opts(Name,Usage,"f:bBlpru:ce:hHx",0,rcFile,
  72.     FileVar ",BRIEF,NOBREAKDOWN,LASTDATE,LASTPAY,RUNNING,UNNAMED",1,"n")
  73.     if (ARGC < 0) {
  74.     print OptErr
  75.     exit 1
  76.     }
  77.     if ((Err = ExclusiveOptions("l,e",Options)) != "") {
  78.     printf "Error: %s\n",Err > "/dev/stderr"
  79.     Err = 1
  80.     exit(1)
  81.     }
  82.     DefFile = ".mbills"
  83.     if ("h" in Options) {
  84.     printf \
  85. "%s: track monthly bill payments.\n"\
  86. "%s\n"\
  87. "%s processes a billing file and uses the information in it to determine\n"\
  88. "the current amount owed by each party.  If no bill-file is named, the\n"\
  89. "default file \"%s\" in the user's home directory is used.  If the\n"\
  90. "environment variable %s is set, its value is used instead.\n"\
  91. "The bill-file describes regular monthly expenses and amounts paid to\n"\
  92. "satisfy those expenses.  After processing, the amount owed by each party\n"\
  93. "is printed, along with a breakdown of how much is owed for each month.  If\n"\
  94. "a party has a net amount owed (the sum of the amounts paid by that party\n"\
  95. "are less than the amounts owed), the amount paid is counted against the\n"\
  96. "oldest bills first; that is, the breakdown of the amount owed will show\n"\
  97. "everything from the end of the period billed for back through the point\n"\
  98. "which has been paid for.\n"\
  99. "If a file named %s exists in the user's home directory, some options\n"\
  100. "can be set in it.  A variable name given for an option may be placed in\n"\
  101. "the file (one per line) to turn that option on.  If the option takes a\n"\
  102. "value, it can be assigned to the variable in %s using the form:\n"\
  103. "variable=value\n"\
  104. "The %s variable can also be assigned a value in %s.\n"\
  105. "Options: \n"\
  106. "-b: Print only a brief summary, without headers or trailers, suitable for\n"\
  107. "    input to another utility.  Variable: BRIEF\n"\
  108. "-B: Do not print a breakdown of what is owed for each month.\n"\
  109. "    Variable: NOBREAKDOWN\n"\
  110. "-h: Print this help.\n"\
  111. "-H: Print a description of the format of a bill file.\n"\
  112. "-l: Bill up through the last day of the month given on the latest rates or\n"\
  113. "    payment line in the input files.  The default is to bill up through\n"\
  114. "    the last day of the current month.  Variable: LASTDATE.\n"\
  115. "-e<date>: Bill up through <date>.  <date> should be given in the form\n"\
  116. "    yy/mm or yy/mm/dd.  If no day of month is given, billing is done up\n"\
  117. "    through the last day of the given month.  All payments are applied\n"\
  118. "    even if a date that precedes some of them is given.\n"\
  119. "-n: Do not read %s file.  Programs that process the output of %s -b\n"\
  120. "    can use this to make sure unwanted options are not turned on.\n"\
  121. "-c: Print rate changes.\n"\
  122. "-r: Print lines from bill files as they are processed.  Variable: RUNNING\n"\
  123. "-p: Also print the date of last payment for each party.  Variable: LASTPAY\n"\
  124. "-u<name>: Set the output name for the unnamed rate type to be <name>.  The\n"\
  125. "    default is \"%s\".  Variable: UNNAMED\n",
  126.     Name,Usage,Name,DefFile,FileVar,hrcFile,hrcFile,FileVar,hrcFile,
  127.     hrcFile,Name,UnnamedName
  128.     exit(0)
  129.     }
  130.     if ("H" in Options) {
  131.     print \
  132. "     Lines in a billing file take one of the following two forms:\n"\
  133. "\n"\
  134. "date [rate-type] party-name=amount ...\n"\
  135. "date <party-name> amount-paid comment\n"\
  136. "\n"\
  137. "     Fields are separated by whitespace.  Dates are given as yy/mm/dd.\n"\
  138. "Blank lines and lines that begin with '#' are considered comment lines and\n"\
  139. "are ignored.  Case is ignored; the capitalization that is given in the\n"\
  140. "first occurance of each bill and party name is used for output purposes.\n"\
  141. "     A line which has only one field after the date or which has fields of\n"\
  142. "the form party-name=value is a rates line.  These lines set the rates for\n"\
  143. "a particular rate type, starting at the given date.  If there was already\n"\
  144. "a rate of the given type in effect, the old rate ends on the day before\n"\
  145. "the new rate begins.  If a rate-type is not given (the first field after\n"\
  146. "the date has an '=' in it), the line sets the rates for the \"unnamed\"\n"\
  147. "rate type.  Using the unnamed rate type can be convenient if there is only\n"\
  148. "one rate type.  \n"\
  149. "     The party-name=amount fields set the monthly rates for the given\n"\
  150. "rate-type.  All rates previously in effect for the rate-type are replaced.\n"\
  151. "Parties who had rates for the given rate-type when its rates were last set\n"\
  152. "and who are not mentioned in the new rates have their rates set to 0 (they\n"\
  153. "are removed from the rate-type).  If there are only two fields on a rates\n"\
  154. "line (there are no party-name=amount fields), the line sets all the rates\n"\
  155. "for the given type to 0 (all parties are removed from it).  This can be\n"\
  156. "used to end billing for a rate type, or to specify a period during which\n"\
  157. "no billing for the rate-type should take place.  The special name 'Rates'\n"\
  158. "refers to the unnamed rate; that can be used to set its rates to 0.\n"\
  159. "     Lines other than comment lines and rates lines are payment lines.  A\n"\
  160. "payment line must have at least two fields after the date.  Payment lines\n"\
  161. "are used to record payments that should be applied toward the bills. \n"\
  162. "Unless the -l or -p option is given, the dates on these lines are not used\n"\
  163. "other than in a check for sequentiality; the amount-paid is simply added\n"\
  164. "to the total for the named party for comparison to the total amount owed. \n"\
  165. "The total amount owed is calculated from the rates lines and extends\n"\
  166. "through the current month, unless -l or -e is given.  The comment field is\n"\
  167. "not required and is not used.\n"\
  168. "Example:\n"\
  169. "92/06/01 Rent Alpha=345 Beta=335 Gamma=345 Delta=270\n"\
  170. "92/06/02 Delta    370    100 Jun, 270 Jul (also paid 100 deposit, 270 last)\n"\
  171. "92/06/03 Beta    120    toward May\n"\
  172. "92/06/09 Alpha    50    Balance of May\n"\
  173. "92/06/21 Gamma    690    Jun, Jul"
  174.     exit 0
  175.     }
  176.     if (ARGC < 2) {
  177.     if ("f" in Options)
  178.         ARGV[1] = Options["f"]
  179.     else
  180.         ARGV[1] = ENVIRON["HOME"] "/" DefFile
  181.     ARGC = 2
  182.     }
  183.     if (PrintChanges = ("c" in Options))
  184.     print "Date     Total Amounts"
  185.     LastPayment = "p" in Options
  186.     Debug = "x" in Options
  187.     if ("u" in Options)
  188.     UnnamedName = Options["u"]
  189.     BillLatest = "l" in Options
  190.     Running = "r" in Options
  191.     Brief = "b" in Options
  192.     NoBreakdown = "B" in Options
  193.     if ("e" in Options) {
  194.     if ((BillThrough = date2day(Options["e"],Fields)) == -1) {
  195.         printf \
  196.         "Bad date \"%s\" given with -e; should be in the form yy/mm/dd\n" \
  197.         > "/dev/stderr"
  198.         Err = 1
  199.         exit 1
  200.     }
  201.     if (!(3 in Fields))
  202.         BillThrough = endOfMonth(BillThrough)
  203.     }
  204. }
  205.  
  206. # Open each file; pass each line to appropriate processing routine.
  207. # Sets globals FILENAME and FNR for ErrExit()
  208. function ProcFiles(ARGC,ARGV,PrintRunning,AllParties,Paid,LastPay,LastAmt,
  209. TotalBilled,RateNames,MRates,MRateChanges,PrintChanges,UnnamedName,Debug,
  210. BillLatest,BillThrough,
  211. ArgInd,Day,LastDay,ret,RateInd,RateName,Rates,FinalDate) {
  212.     for (ArgInd = 1; ArgInd < ARGC; ArgInd++) {
  213.     FILENAME = ARGV[ArgInd]
  214.     FNR = 0
  215.     while ((ret = (getline < FILENAME)) == 1) {
  216.         FNR++
  217.         if (!NF)
  218.         continue
  219.         if (PrintRunning == 1)
  220.         print $0
  221.         if ($1 ~ /^#/)
  222.         continue
  223.         if ((Day = date2day($1)) == -1)
  224.         ErrExit("Bad date.")
  225.         if (Day < LastDay)
  226.         ErrExit("Line out of date sequence.")
  227.         LastDay = Day
  228.         # A rates line is one that:
  229.         # has an = in the 2nd or 3rd field (unnamed or named rate),
  230.         # or which has only 2 fields (setting a named rate to 0).
  231.         if (index($2,"=") || NF == 2 || index($3,"=")) {
  232.         if (BillThrough && Day > BillThrough) {
  233.             if (Debug)
  234.             printf "Ignoring rate line with date %s\n",
  235.             $1 > "/dev/stderr"
  236.             continue
  237.         }
  238.         RateInd = 2
  239.         # If the first rate field doesn't assign a rate value,
  240.         # it is the name of this rate
  241.         if ($RateInd !~ "=") {
  242.             RateName = ($RateInd == "Rates") ? UnnamedName : $RateInd
  243.             RateInd++
  244.         }
  245.         else
  246.             RateName = UnnamedName
  247.         # Allow 2nd field to be Rates for backward compatibility
  248.         ChangeRates(Day,TotalBilled,MRates,MRateChanges,RateName,
  249.         AllParties,Rates,RateNames,RateInd,Debug,PrintChanges)
  250.         }
  251.         else
  252.         PaymentLine(Day,AllParties,Paid,LastPay,LastAmt)
  253.     }
  254.     if (ret)
  255.         ErrExit("Error reading file: " ERRNO)
  256.     }
  257.     # Bill at the final rate for the period up to the last day of
  258.     # the last month seen or the current month, or up to a specific date.
  259.     if (BillLatest)
  260.     FinalDate = endOfMonth(LastDay)+1
  261.     else if (BillThrough)
  262.     FinalDate = BillThrough+1
  263.     else
  264.     FinalDate = -1
  265.     for (RateName in RateNames)
  266.     ChangeRates(FinalDate,TotalBilled,MRates,MRateChanges,
  267.     RateName,AllParties,Rates,RateNames,0,Debug)
  268. }
  269.  
  270. # Accumulate amounts paid.
  271. # Uses globals $*
  272. function PaymentLine(Day,AllParties,Paid,LastPay,LastAmt,  Party) {
  273.     Party = tolower($2)
  274.     if (!(Party in AllParties))
  275.     ErrExit("Unknown party \"" $2 "\".")
  276.     if ($3 !~ "^[0-9]+$")
  277.     ErrExit("Bad rate \"" Amount "\".")
  278.     Paid[Party] += $3
  279.     # Entries are checked for date monotonicity elsewhere
  280.     LastPay[Party] = Day
  281.     LastAmt[Party] = $3
  282. }
  283.  
  284. # OwedForMonths generates a string describing how much is owed for each month,
  285. # starting with the current month and continuing back in time until the
  286. # entire amount owed is accounted for.
  287. # Party is the party to find amounts owed by.
  288. # Month is the month to start at (the final month that is being billed for).
  289. # Amount is the amount owed.
  290. # RateFind[RateType,Party,Month] tells how much is owed for each month.
  291. # RateNames[] is the set of rate names used as part of the index of RateFind[];
  292. #   the value is the mixed-case version of the rate name for printing.
  293. # FirstMonth is the first month that was billed for, for safety.
  294. function OwedForMonths(Party,Month,Amount,RateFind,RateNames,FirstMonth,
  295. Num2Month,
  296. S,ForThisMonth,Rate,RateName) {
  297.     for (; Amount > 0 && Month >= FirstMonth; Month--) {
  298.     ForThisMonth = 0
  299.     for (RateName in RateNames)
  300.         if ((RateName,Month) in RateFind)
  301.         ForThisMonth += min(Amount,
  302.         Rate = int(MRates[RateName,RateFind[RateName,Month],Party]+0.5))
  303.         else {
  304.         if (Debug)
  305.             print "No rate for %s for month %s\n",RateName,Month \
  306.             > "/dev/stderr"
  307.         continue
  308.         }
  309.     if (ForThisMonth > 0)
  310.         S = S sprintf("; %s: $%d",Num2Month[Month % 12 + 1],ForThisMonth)
  311.     if (Amount < Rate)
  312.         S = S sprintf(" of $%d",Rate)
  313.     Amount -= ForThisMonth
  314.     }
  315.     if (Month < FirstMonth) {
  316.     printf "Error in OwedForMonths():\n"\
  317.     "Left with balance of %d while generating string for %s\n",Amount,Party
  318.     Err = 1
  319.     exit 1
  320.     }
  321.     return substr(S,3)
  322. }
  323.  
  324. # Generate RateFind[] and FinalParties[]
  325. function MakeRateFind(RateNames,MRates,RateFind,Debug,AllParties,FinalParties,
  326. RateName,LastRateChange,Month,LastMonth,FirstMonth,Party) {
  327.     FirstMonth = day2month(MRates["FirstDay"])
  328.     # MRates["LastDay"] is the day of the last rate change, which will be the
  329.     # pseudo-ratechange that is used to finish billing for the last real rate
  330.     # change.  It is the day after the real rates ended.
  331.     # So, subtract 1 from it to get the final day of actual billing.
  332.     LastMonth = day2month(MRates["LastDay"] - 1)
  333.     for (RateName in RateNames) {    # Build RateFind[]
  334.     LastRateChange = ""
  335.     for (Month = FirstMonth; Month <= LastMonth; Month++) {
  336.         if ((RateName,Month) in MRateChanges) {
  337.         LastRateChange = Month
  338.         if (Debug) {
  339.             printf "Prorated rates for %s for %s:",RateNames[RateName],
  340.             month2date(Month) > "/dev/stderr"
  341.             for (Party in AllParties)
  342.             if ((RateName,Month,Party) in MRates)
  343.                 printf " %s=%s",AllParties[Party],
  344.                 MRates[RateName,Month,Party] > "/dev/stderr"
  345.             print "" > "/dev/stderr"
  346.         }
  347.         }
  348.         # Make RateFind[ratetype,month] map the month to the index in
  349.         # Rates[] that gives the rate that was in effect that month.
  350.         if (LastRateChange != "") {
  351.         RateFind[RateName,Month] = LastRateChange
  352.         if (Debug)
  353.             printf "%s rate for %s was set %s\n",RateNames[RateName],
  354.             month2date(Month),month2date(LastRateChange) > "/dev/stderr"
  355.         }
  356.     }
  357.     }
  358.     if (Debug)
  359.     print "" > "/dev/stderr"
  360.  
  361.     # Find all parties who are party to at least one of the rates at the time
  362.     # billing ended.
  363.     # Use RateFind[] to find the last rate setting for each rate type.
  364.     for (RateName in RateNames)
  365.     for (Party in AllParties)
  366.         if ((RateName,LastMonth) in RateFind && 
  367.         (RateName,RateFind[RateName,LastMonth],Party) in MRates)
  368.         FinalParties[Party]
  369.     if (Debug) {
  370.     printf "Final parties:" > "/dev/stderr"
  371.     for (Party in FinalParties)
  372.         printf " %s",AllParties[Party] > "/dev/stderr"
  373.     printf "\n" > "/dev/stderr"
  374.     }
  375. }
  376.  
  377. function PrintResults(AllParties,Paid,LastPay,LastAmt,Brief,NoBreakdown,
  378. TotalBilled,RateNames,MRates,MRateChanges,LastPayment,Debug,
  379. RateFind,Party,Owed,Format,CWidth,OwedWidth,Date,LastMonth,LastDay,t,Tot,
  380. FirstDay,RateName,FinalParties,FirstMonth,LastRateChange,Month,TotOwed,
  381. Num2Month) {
  382.  
  383.     FirstMonth = day2month(FirstDay = MRates["FirstDay"])
  384.     # Since the last day is always set to the date of a rate change,
  385.     # and a psuedo-ratechange is done for the first day of the next month after
  386.     # the current month, MRates["LastDay"] will hold that day.
  387.     # Subtract one from it to get the last day of the last month that was
  388.     # billed for.
  389.     LastMonth = day2month(LastDay = MRates["LastDay"] - 1)
  390.     if (Debug)
  391.     print "" > "/dev/stderr"
  392.     MakeRateFind(RateNames,MRates,RateFind,Debug,AllParties,FinalParties)
  393.  
  394.  
  395.     split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",Num2Month,",")
  396.     OwedWidth = 4    # Width for amount-owed field
  397.     if (Brief)
  398.     for (Party in AllParties) {
  399.         Tot = int(TotalBilled[Party] + 0.5)
  400.         Owed = Tot - Paid[Party]
  401.         if (Owed < 1 && !(Party in FinalParties))
  402.         delete AllParties[Party]
  403.         else {
  404.         printf "%-8s %" OwedWidth "d",AllParties[Party],Tot-Paid[Party]
  405.         if (NoBreakdown)
  406.             print ""
  407.         else
  408.             printf " %s\n",
  409.             OwedForMonths(Party,LastMonth,Owed,RateFind,RateNames,
  410.             FirstMonth,Num2Month)
  411.         }
  412.     }
  413.     else {
  414.     printf "Period: %s to %s.  Rate types:",day2date(FirstDay),
  415.     day2date(LastDay)
  416.     for (RateName in RateNames)
  417.         printf " %s",RateNames[RateName]
  418.     print ""
  419.     CWidth = 5    # digits for cumulative values (total & paid)
  420.     Format = "%-8s %" CWidth "s %" CWidth "s %" OwedWidth "s"
  421.     printf Format,"Party","Total","Paid","Owed"
  422.     if (NoBreakdown)
  423.         print ""
  424.     else
  425.         print " Breakdown"
  426.     for (Party in AllParties) {
  427.         Tot = int(TotalBilled[Party] + 0.5)
  428.         Owed = Tot - Paid[Party]
  429.         if (Debug)
  430.         printf "%s owes %.2f\n",AllParties[Party],Owed > "/dev/stderr"
  431.         if (Owed < 1 && !(Party in FinalParties))
  432.         delete AllParties[Party]
  433.         else {
  434.         printf Format,AllParties[Party],Tot,Paid[Party]+0,Owed
  435.         if (NoBreakdown)
  436.             print ""
  437.         else
  438.             print " " \
  439.             OwedForMonths(Party,LastMonth,Owed,RateFind,RateNames,
  440.             FirstMonth,Num2Month)
  441.         TotOwed += Owed
  442.         }
  443.     }
  444.     printf "Total owed: %.2f\n",TotOwed
  445.     }
  446.     if (LastPayment) {
  447.     printf "\nLast payments:\n"
  448.     for (Party in AllParties)
  449.         printf "%-10s  %8s  %5s\n",AllParties[Party],
  450.         Party in LastPay ? day2date(LastPay[Party]) : "NEVER",
  451.         Party in LastAmt ? "$" LastAmt[Party] : ""
  452.     }
  453. }
  454.  
  455. ## Library routines
  456.  
  457. ### Start of mbill library
  458.  
  459. ## Start of rate change processing routines
  460. # ChangeRates: process a rate-change line.
  461. # RateName is the rate type.
  462. # Add the amounts owed from the last rate change to this rate change to the
  463. #   balances (TotalBilled[party]) for all of the parties who were included in
  464. #   the last rate change.
  465. # Record the new rates in Rates[ratename,party] so that the same can be done
  466. #   when the next rate change occurs.
  467. # Record the rates and the month they took effect in
  468. #   MRates[ratename,month,party] so that if there is an unpaid balance the
  469. #   amounts owed for each month can be displayed.
  470. # Set MRateChanges[RateName,Month] so that the data in MRates[] can be found.
  471. # Make each party an index of AllParties[], so that error checking can be
  472. #   done on payment lines.
  473. # Set MRates["FirstDay"] to the day of the first Rates line processed and
  474. #   MRates["LastDay"] to the day for this rate change for anything that
  475. #   wants to iterate over all months.
  476. # Set MRates[ratename,"FirstDay"] to the day of the first Rates line found for
  477. #   this rate type, and MRates[ratename,"LastDay"] to the day for this rate
  478. #   change for anything that needs to know the first & last month for each
  479. #   rate type.
  480. # Makes every rate name encountered an index (in lower case) of RateNames[],
  481. #   with the value being the name in mixed case as first encountered.
  482. # After the last rate change, ChangeRates should be called once more for each
  483. #   rate type to process up to the current day, with RateInd set to 0 to
  484. #   indicate that no new amounts should be added.
  485. #   The last parameter does not need to be passed in this case.
  486. #   If -1 is passed for Day, billing will be done for the period from the last
  487. #   rate change up to the last day of the current month.
  488. #
  489. # Globals: $*
  490. #
  491. # Uses month2day(), month2days(), month2date(), day2date(), ErrExit(), Bill()
  492. # Day is the unix-day of the rate change.  $RateInd..$NF are the rates.
  493. #
  494. function ChangeRates(Day,TotalBilled,MRates,MRateChanges,RateName,
  495. AllParties,Rates,RateNames,RateInd,Debug,PrintChanges,
  496. DateF,Month,i,Party,Amount,Total,t,CurMonth,MixedName,MixedParty) {
  497.     RateName = tolower(MixedName = RateName)
  498.     if (Day == -1) {
  499.     # Get time beforehand so that all strftime()s will get the same time
  500.     t = systime()
  501.     CurMonth = (strftime("%Y",t)-1970)*12+strftime("%m",t)-1
  502.     Day = month2day(CurMonth) + monthdays(CurMonth)
  503.     if (Debug) {
  504.         printf "\nSetting last month of billing period for %s to: %s\n",
  505.         RateNames[RateName],month2date(CurMonth) > "/dev/stderr"
  506.         printf "Setting last day of billing period for %s to: %s\n",
  507.         RateNames[RateName],day2date(Day-1) > "/dev/stderr"
  508.     }
  509.     }
  510.     if (!("FirstDay" in MRates)) {
  511.     MRates["FirstDay"] = Day
  512.     if (Debug)
  513.         printf "First day: %s\n",day2date(Day) > "/dev/stderr"
  514.     }
  515.     # If this is the first line for this rate
  516.     if (!((RateName,"FirstDay") in MRates)) {
  517.     RateNames[RateName] = MixedName
  518.     MRates[RateName,"FirstDay"] = Day
  519.     if (Debug)
  520.         printf "First day for %s: %s\n",RateNames[RateName],day2date(Day) \
  521.         > "/dev/stderr"
  522.     }
  523.     else
  524.     Bill(RateName,RateNames,MRates[RateName,"LastDay"],Day,Rates,
  525.     TotalBilled,MRates,MRateChanges,AllParties,Debug)
  526.     MRates["LastDay"] = Day
  527.     MRates[RateName,"LastDay"] = Day
  528.     # If doing final billing, no more to do
  529.     if (!RateInd)    
  530.     return
  531.     for (Party in AllParties)
  532.     delete Rates[RateName,Party]
  533.     for (i = RateInd; i <= NF; i++) {
  534.     MixedParty = Amount = $i
  535.     sub("=[^=]*","",MixedParty)
  536.     sub(".*=","",Amount)
  537.     if (MixedParty !~ "^[a-zA-Z]+$")
  538.         ErrExit("Bad party name \""  MixedParty "\" on Rates line.")
  539.     if (Amount !~ "^[0-9]+")
  540.         ErrExit("Bad rate \""  Amount "\" on Rates line.")
  541.     Party = tolower(MixedParty)
  542.     Total += Rates[RateName,Party] = Amount
  543.     if (!(Party in AllParties)) {
  544.         AllParties[Party] = MixedParty
  545.         if (Debug)
  546.         printf "New party: %s\n",MixedParty > "/dev/stderr"
  547.     }
  548.     }
  549.     if (Debug)
  550.     print "Rate line for next period: " $0 > "/dev/stderr"
  551.     if (PrintChanges) {
  552.     $2 = sprintf("%5d",Total)
  553.     print $0
  554.     }
  555. }
  556.  
  557. # RateName: The rate type being billed for.
  558. # StartDay: The day the rates went into effect.
  559. # EndDay: the day the rate change occured (billing is done up through the
  560. #      previous day).
  561. # Rates[ratename,party]: the rates in effect for the period to be billed for.
  562. # TotalBilled[party] is incremented for the last billing period for each party.
  563. # Creates the index MRateChanges[RateName,Month].
  564. # MRates[ratename,month,party] is set to the monthly rate for each party.
  565. # Uses day2YMD(), day2date(), month2date(), SBill()
  566. function Bill(RateName,RateNames,StartDay,EndDay,Rates,TotalBilled,MRates,
  567. MRateChanges,AllParties,Debug,
  568. Month,StartMonth,EndMonth,StartDate,EndDate,StartDOM,EndDOM,
  569. StartNDays,EndNDays) {
  570.     day2YMD(StartDay,StartDate)
  571.     day2YMD(--EndDay,EndDate)
  572.     StartMonth = day2month(StartDay)
  573.     EndMonth = day2month(EndDay)
  574.     StartDOM = StartDate["d"]
  575.     EndDOM = EndDate["d"]
  576.     StartNDays = monthdays(StartMonth)
  577.     EndNDays = monthdays(EndMonth)
  578.     if (Debug)
  579.     printf "\nPeriod: %s-%s   "\
  580.     "Days in 1st mo: %d  Days in last mo: %d\n", day2date(StartDay),
  581.     day2date(EndDay), StartNDays,EndNDays > "/dev/stderr"
  582.     if (StartMonth == EndMonth) {
  583.     if (Debug)
  584.         printf \
  585.     "Start and end month are the same (%s); billing for %d days, %f mo.\n",
  586.         month2date(StartMonth),(EndDOM-StartDOM+1),
  587.         (EndDOM-StartDOM+1)/StartNDays > "/dev/stderr"
  588.     SBill(RateName,RateNames,Rates,(EndDOM-StartDOM+1)/StartNDays,
  589.     TotalBilled,StartMonth,MRates,MRateChanges,AllParties,Debug)
  590.     return
  591.     }
  592.     if (StartDOM != 1) {
  593.     if (Debug)
  594.         printf \
  595.         "Billing for fractional first month %s (%d days, %f month).\n",
  596.         month2date(StartMonth),(StartNDays-StartDOM+1),
  597.         (StartNDays-StartDOM+1)/StartNDays > "/dev/stderr"
  598.     SBill(RateName,RateNames,Rates,(StartNDays-StartDOM+1)/StartNDays,
  599.     TotalBilled,StartMonth,MRates,MRateChanges,AllParties,Debug)
  600.     StartMonth++    # Have dealt with start month now
  601.     }
  602.     if (EndDOM != EndNDays) {
  603.     if (Debug)
  604.         printf \
  605.         "Billing for fractional last month %s (%d days, %f month).\n",
  606.         month2date(EndMonth),(EndNDays-EndDOM+1),
  607.         (StartNDays-StartDOM+1)/StartNDays > "/dev/stderr"
  608.     SBill(RateName,RateNames,Rates,EndDOM/EndNDays,TotalBilled,EndMonth,
  609.     MRates,MRateChanges,AllParties,Debug)
  610.     EndMonth--
  611.     }
  612.     if ((EndMonth - StartMonth) >= 0) {
  613.     if (Debug)
  614.         printf "Billing for %d full month(s) (%s-%s)\n",
  615.         EndMonth-StartMonth+1,month2date(StartMonth),month2date(EndMonth) \
  616.         > "/dev/stderr"
  617.     SBill(RateName,RateNames,Rates,EndMonth-StartMonth+1,TotalBilled,
  618.     StartMonth,MRates,MRateChanges,AllParties,Debug)
  619.     }
  620. }
  621.  
  622. # RateName: The rate type being billed for.
  623. # Rates[ratename,party]: the rates in effect for the period to be billed for.
  624. # Months: The number of months (may be non-integer) that the rates were in
  625. # effect.
  626. # The amount each party owes for this period is added to TotalBilled[party].
  627. # Makes (RateName,StartMonth) an index of MRateChanges[].
  628. # If a fractional month is being processed, the amount billed is added to
  629. # MRates[ratename,month,party].  If full months are being processed,
  630. # MRates[ratename,month,party] is set to the monthly rate.
  631. function SBill(RateName,RateNames,Rates,Months,TotalBilled,StartMonth,MRates,
  632. MRateChanges,AllParties,Debug,  Party,Amount) {
  633.     MRateChanges[RateName,StartMonth]
  634.     for (Party in AllParties) {
  635.     if ((RateName,Party) in Rates) {
  636.         TotalBilled[Party] += Amount = Rates[RateName,Party] * Months
  637.         if (Months < 1)
  638.         MRates[RateName,StartMonth,Party] += Amount
  639.         else
  640.         MRates[RateName,StartMonth,Party] = Rates[RateName,Party]
  641.         if (Debug) {
  642.         if (!(RateName in RateNames))
  643.             printf "%s not found in RateNames[]?!\n",
  644.             RateName > "/dev/stderr"
  645.         else
  646.             printf \
  647.         "Billing %s $%.2f for %.2g mo. of %s @ %.2f/mo.; total = $%.2f\n",
  648.             AllParties[Party],Amount,Months,RateNames[RateName],
  649.             Rates[RateName,Party],TotalBilled[Party] > "/dev/stderr"
  650.         }
  651.     }
  652.     else if (Debug)
  653.         printf "%s is not a party to rate %s\n",AllParties[Party],
  654.         RateNames[RateName] > "/dev/stderr"
  655.     }
  656. }
  657. ## End of rate change processing routines
  658. ### End of mbill library
  659.  
  660. ### Begin date-days routines
  661. # @(#) date-days 1.2 95/12/18
  662. # 95/12/18 Added endOfMonth()
  663. #          Fixed date2day() to work correctly with no month-day specified.
  664.  
  665. # YMD2day(year,month,day-of-month) returns the number of days that passed from 
  666. # 1970 Jan 1 to the given date.
  667. # All parameters should be given in numeric form.
  668. # If year < 70, it is assumed to be part of the 2000 century
  669. # If year in (70..99), 1900.
  670. # Globals: sets and uses MDays[]
  671. function YMD2day(Year,Month,Day,   LeapDays) {
  672.     Year+=0
  673.     Month+=0
  674.     if (Year < 70)
  675.     Year += 100
  676.     else if (Year >= 100)
  677.     Year -= 1900
  678.     # Year is now the number of years since 1900.
  679.     LeapDays = int((Year - 68) / 4)
  680.     if (Month <= 2 && Year % 4 == 0)
  681.     LeapDays -= 1
  682.     if (!(0 in MDays))
  683.     split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays," ")
  684.     return (Year - 70) * 365 + MDays[Month] + Day - 1 + LeapDays
  685. }
  686.  
  687. # date2day("yy/mm/dd") returns the number of days that passed from 
  688. # 1970 Jan 1 to the given date.  -1 is returned on error.
  689. # The fields are returned in Fields: year in Fields[1], month in Fields[2],
  690. # and day (if given) in Fields[3].
  691. # If the day is not given, the first of the month is used.
  692. function date2day(Date,Fields,  Num,Year,Month) {
  693.     Num = split(Date,Fields,"/")
  694.     if (Num != 2 && Num != 3)
  695.     return -1
  696.     if (!(Year = Fields[1] + 0) || !(Month = Fields[2] + 0))
  697.     return -1
  698.     if (Num == 3)
  699.     Day = Fields[3]
  700.     return YMD2day(Year,Month,Num == 3 ? Day : 1)
  701. }
  702.  
  703. # diffdays(year1,month1,day-of-month1,year2,month2,day-of-month2)
  704. # returns the number of complete days that passed from date 1 to date 2
  705. function diffdays(year1,month1,day1,year2,month2,day2) {
  706.     return date2days(year2,month2,day2) - date2days(year1,month1,day1)
  707. }
  708.  
  709. # Given an epoch month, return the first day of that month
  710. function month2day(Month) {
  711.     return YMD2day(int(Month/12) + 1970,Month % 12 + 1,1)
  712. }
  713.  
  714. # Given an epoch day, returns the epoch day of the last day of that month
  715. function endOfMonth(Day,  Month) {
  716.     Month = day2month(Day)
  717.     return month2day(Month) + monthdays(Month) - 1
  718. }
  719.  
  720. # Given an epoch day, returns epoch month
  721. function day2month(Day,  Date) {
  722.     day2YMD(Day,Date)
  723.     return (Date["y"]-1970)*12 + Date["m"]-1
  724. }
  725.  
  726. # Given an epoch month, returns the number of days in that month.
  727. function monthdays(month,  year) {
  728.     if (!(0 in MDur))
  729.     split("31 28 31 30 31 30 31 31 30 31 30 31",MDur)
  730.     year = int(month/12)
  731.     month = month%12+1
  732.     return (!((year+2)%4) && month == 2) ? 29 : MDur[month]
  733. }
  734.  
  735. # Given an epoch day (day since 1970 Jan 1; day 0 = 1970 Jan 1, etc.), 
  736. # returns the date elements in Date:
  737. # Date["y"] = year (4 digits), Date["m"] = month (jan = 1, etc.),
  738. # Date["d"] = day of month.
  739. # Globals: Sets/uses MDays[].
  740. function day2YMD(Day,Date,  QYears,Year,NonLeapYears,Month) {
  741.     if (!(0 in LDays)) {
  742.     split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays," ")
  743.     split("0 31 60 91 121 152 182 213 244 274 305 335 366",LDays," ")
  744.     }
  745.     Day += 365
  746.     # Day is now # of days since Jan 1 1969.  1968 was a leap year.
  747.     QYears = int(Day / (365*4+1))
  748.     Year = 1969 + QYears * 4
  749.     Day -= QYears * (365*4+1)
  750.     # Day now contains no complete leap years.
  751.     Year += NonLeapYears = int(Day/365)
  752.     Leap = !(Year % 4)
  753.     Day -= NonLeapYears * 365
  754.     # Day now contains the day of year.
  755.     # Find the month.  Divide day by 32 to get either the correct month or
  756.     # the month prior to it.
  757.     Month = int(Day++ / 32) + 1
  758.     if (Day > (Leap ? LDays[Month+1] : MDays[Month+1]))
  759.     Month++
  760.     Day -= Leap ? LDays[Month] : MDays[Month]
  761.     Date["d"] = Day
  762.     Date["m"] = Month
  763.     Date["y"] = Year
  764. }
  765.  
  766. # Given a month number, return a date in the form yy/mm
  767. function month2date(MonthNum) {
  768.     return sprintf("%02d/%02d",(MonthNum / 12 + 70) % 100, MonthNum % 12 + 1)
  769. }
  770.  
  771. # Given a day number, return a date in the form yy/mm/dd
  772. function day2date(day) {
  773.     day2YMD(day,Date)
  774.     return sprintf("%02d/%02d/%02d",Date["y"]%100,Date["m"],Date["d"])
  775. }
  776.  
  777. ### End date-days routines
  778.  
  779. function ErrExit(S) {
  780.     if (!FNR)
  781.     # If a failure occurs before any lines are read, ignore the string
  782.     # passed and print errno instead.
  783.     printf "Could not read file \"%s\": %s\n",FILENAME,ERRNO
  784.     else
  785.     printf "Error on line %d of file \"%s\":\n%s\n^^^ %s\n",
  786.     FNR,FILENAME,$0,S > "/dev/stderr"
  787.     exit 1
  788. }
  789.  
  790. function min(a,b) {
  791.     if (a < b)
  792.     return a
  793.     else
  794.     return b
  795. }
  796.  
  797. ### Start of ProcArgs library
  798. # @(#) ProcArgs 1.11 96/12/08
  799. # 92/02/29 john h. dubois iii (john@armory.com)
  800. # 93/07/18 Added "#" arg type
  801. # 93/09/26 Do not count -h against MinArgs
  802. # 94/01/01 Stop scanning at first non-option arg.  Added ">" option type.
  803. #          Removed meaning of "+" or "-" by itself.
  804. # 94/03/08 Added & option and *()< option types.
  805. # 94/04/02 Added NoRCopt to Opts()
  806. # 94/06/11 Mark numeric variables as such.
  807. # 94/07/08 Opts(): Do not require any args if h option is given.
  808. # 95/01/22 Record options given more than once.  Record option num in argv.
  809. # 95/06/08 Added ExclusiveOptions().
  810. # 96/01/20 Let rcfiles be a colon-separated list of filenames.
  811. #          Expand $VARNAME at the start of its filenames.
  812. #          Let varname=0 and -option- turn off an option.
  813. # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many
  814. #          of the vars should be searched for in the environment.
  815. #          Check for duplicate rcfiles.
  816. # 96/05/13 Return more specific error values.  Note: ProcArgs() and InitOpts()
  817. #          now return various negatives values on error, not just -1, and
  818. #          Opts() may set Err to various positive values, not just 1.
  819. #          Added AllowUnrecOpt.
  820. # 96/05/23 Check type given for & option
  821. # 96/06/15 Re-port to awk
  822. # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be
  823. #          used by other functions.
  824. # 96/10/15 Added OptChars
  825. # 96/11/01 Added exOpts arg to Opts()
  826. # 96/11/16 Added ; type
  827. # 96/12/08 Added Opt2Set() & Opt2Sets()
  828. # 96/12/27 Added CmdLineOpt()
  829.  
  830. # optlist is a string which contains all of the possible command line options.
  831. # A character followed by certain characters indicates that the option takes
  832. # an argument, with type as follows:
  833. # :    String argument
  834. # ;    Non-empty string argument
  835. # *    Floating point argument
  836. # (    Non-negative floating point argument
  837. # )    Positive floating point argument
  838. # #    Integer argument
  839. # <    Non-negative integer argument
  840. # >    Positive integer argument
  841. # The only difference the type of argument makes is in the runtime argument
  842. # error checking that is done.
  843.  
  844. # The & option is a special case used to get numeric options without the
  845. # user having to give an option character.  It is shorthand for [-+.0-9].
  846. # If & is included in optlist and an option string that begins with one of
  847. # these characters is seen, the value given to "&" will include the first
  848. # char of the option.  & must be followed by a type character other than ":"
  849. # or ";".
  850. # Note that if e.g. &> is given, an option of -.5 will produce an error.
  851.  
  852. # Strings in argv[] which begin with "-" or "+" are taken to be
  853. # strings of options, except that a string which consists solely of "-"
  854. # or "+" is taken to be a non-option string; like other non-option strings,
  855. # it stops the scanning of argv and is left in argv[].
  856. # An argument of "--" or "++" also stops the scanning of argv[] but is removed.
  857. # If an option takes an argument, the argument may either immediately
  858. # follow it or be given separately.
  859. # "-" and "+" options are treated the same.  "+" is allowed because most awks
  860. # take any -options to be arguments to themselves.  gawk 2.15 was enhanced to
  861. # stop scanning when it encounters an unrecognized option, though until 2.15.5
  862. # this feature had a flaw that caused problems in some cases.  See the OptChars
  863. # parameter to explicitly set the option-specifier characters.
  864.  
  865. # If an option that does not take an argument is given,
  866. # an index with its name is created in Options and its value is set to the
  867. # number of times it occurs in argv[].
  868.  
  869. # If an option that does take an argument is given, an index with its name is
  870. # created in Options and its value is set to the value of the argument given
  871. # for it, and Options[option-name,"count"] is (initially) set to the 1.
  872. # If an option that takes an argument is given more than once,
  873. # Options[option-name,"count"] is incremented, and the value is assigned to
  874. # the index (option-name,instance) where instance is 2 for the second occurance
  875. # of the option, etc.
  876. # In other words, the first time an option with a value is encountered, the
  877. # value is assigned to an index consisting only of its name; for any further
  878. # occurances of the option, the value index has an extra (count) dimension.
  879.  
  880. # The sequence number for each option found in argv[] is stored in
  881. # Options[option-name,"num",instance], where instance is 1 for the first
  882. # occurance of the option, etc.  The sequence number starts at 1 and is
  883. # incremented for each option, both those that have a value and those that
  884. # do not.  Options set from a config file have a value of 0 assigned to this.
  885.  
  886. # Options and their arguments are deleted from argv.
  887. # Note that this means that there may be gaps left in the indices of argv[].
  888. # If compress is nonzero, argv[] is packed by moving its elements so that
  889. # they have contiguous integer indices starting with 0.
  890. # Option processing will stop with the first unrecognized option, just as
  891. # though -- was given except that unlike -- the unrecognized option will not be
  892. # removed from ARGV[].  Normally, an error value is returned in this case.
  893. # If AllowUnrecOpt is true, it is not an error for an unrecognized option to
  894. # be found, so the number of remaining arguments is returned instead.
  895. # If OptChars is not a null string, it is the set of characters that indicate
  896. # that an argument is an option string if the string begins with one of the
  897. # characters.  A string consisting solely of two of the same option-indicator
  898. # characters stops the scanning of argv[].  The default is "-+".
  899. # argv[0] is not examined.
  900. # The number of arguments left in argc is returned.
  901. # If an error occurs, the global string OptErr is set to an error message
  902. # and a negative value is returned.
  903. # Current error values:
  904. # -1: option that required an argument did not get it.
  905. # -2: argument of incorrect type supplied for an option.
  906. # -3: unrecognized (invalid) option.
  907. function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars,
  908. ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven,
  909. NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet)
  910. {
  911. # ArgNum is the index of the argument being processed.
  912. # ArgsLeft is the number of arguments left in argv.
  913. # Arg is the argument being processed.
  914. # ArgLen is the length of the argument being processed.
  915. # ArgInd is the position of the character in Arg being processed.
  916. # Option is the character in Arg being processed.
  917. # Pos is the position in OptList of the option being processed.
  918. # NumOpt is true if a numeric option may be given.
  919.     ArgsLeft = argc
  920.     NumOpt = index(OptList,"&")
  921.     OptionNum = 0
  922.     if (OptChars == "")
  923.     OptChars = "-+"
  924.     while (OptChars != "") {
  925.     c = substr(OptChars,1,1)
  926.     OptChars = substr(OptChars,2)
  927.     OptCharSet[c]
  928.     OptTerm[c c]
  929.     }
  930.     for (ArgNum = 1; ArgNum < argc; ArgNum++) {
  931.     Arg = argv[ArgNum]
  932.     if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet))
  933.         break    # Not an option; quit
  934.     if (Arg in OptTerm) {
  935.         delete argv[ArgNum]
  936.         ArgsLeft--
  937.         break
  938.     }
  939.     ArgLen = length(Arg)
  940.     for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) {
  941.         Option = substr(Arg,ArgInd,1)
  942.         if (NumOpt && Option ~ /[-+.0-9]/) {
  943.         # If this option is a numeric option, make its flag be & and
  944.         # its option string flag position be the position of & in
  945.         # the option string.
  946.         Option = "&"
  947.         Pos = NumOpt
  948.         # Prefix Arg with a char so that ArgInd will point to the
  949.         # first char of the numeric option.
  950.         Arg = "&" Arg
  951.         ArgLen++
  952.         }
  953.         # Find position of flag in option string, to get its type (if any).
  954.         # Disallow & as literal flag.
  955.         else if (!(Pos = index(OptList,Option)) || Option == "&") {
  956.         if (AllowUnrecOpt) {
  957.             Escape = 1
  958.             break
  959.         }
  960.         else {
  961.             OptErr = "Invalid option: " specGiven Option
  962.             return -3
  963.         }
  964.         }
  965.  
  966.         # Find what the value of the option will be if it takes one.
  967.         # NeedNextOpt is true if the option specifier is the last char of
  968.         # this arg, which means that if the option requires a value it is
  969.         # the next arg.
  970.         if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg
  971.         if (GotValue = ArgNum + 1 < argc)
  972.             Value = argv[ArgNum+1]
  973.         }
  974.         else {    # Value is included with option
  975.         Value = substr(Arg,ArgInd + 1)
  976.         GotValue = 1
  977.         }
  978.  
  979.         if (HadValue = AssignVal(Option,Value,Options,
  980.         substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt,
  981.         specGiven)) {
  982.         if (HadValue < 0)    # error occured
  983.             return HadValue
  984.         if (HadValue == 2)
  985.             ArgInd++    # Account for the single-char value we used.
  986.         else {
  987.             if (NeedNextOpt) {    # option took next arg as value
  988.             delete argv[++ArgNum]
  989.             ArgsLeft--
  990.             }
  991.             break    # This option has been used up
  992.         }
  993.         }
  994.     }
  995.     if (Escape)
  996.         break
  997.     # Do not delete arg until after processing of it, so that if it is not
  998.     # recognized it can be left in ARGV[].
  999.     delete argv[ArgNum]
  1000.     ArgsLeft--
  1001.     }
  1002.     if (compress != 0) {
  1003.     dest = 1
  1004.     src = argc - ArgsLeft + 1
  1005.     for (count = ArgsLeft - 1; count; count--) {
  1006.         ARGV[dest] = ARGV[src]
  1007.         dest++
  1008.         src++
  1009.     }
  1010.     }
  1011.     return ArgsLeft
  1012. }
  1013.  
  1014. # Assignment to values in Options[] occurs only in this function.
  1015. # Option: Option specifier character.
  1016. # Value: Value to be assigned to option, if it takes a value.
  1017. # Options[]: Options array to return values in.
  1018. # ArgType: Argument type specifier character.
  1019. # GotValue: Whether any value is available to be assigned to this option.
  1020. # Name: Name of option being processed.
  1021. # OptionNum: Number of this option (starting with 1) if set in argv[],
  1022. #     or 0 if it was given in a config file or in the environment.
  1023. # SingleOpt: true if the value (if any) that is available for this option was
  1024. #     given as part of the same command line arg as the option.  Used only for
  1025. #     options from the command line.
  1026. # specGiven is the option specifier character use, if any (e.g. - or +),
  1027. # for use in error messages.
  1028. # Global variables: OptErr
  1029. # Return value: negative value on error, 0 if option did not require an
  1030. # argument, 1 if it did & used the whole arg, 2 if it required just one char of
  1031. # the arg.
  1032. # Current error values:
  1033. # -1: Option that required an argument did not get it.
  1034. # -2: Value of incorrect type supplied for option.
  1035. # -3: Bad type given for option &
  1036. function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum,
  1037. SingleOpt,specGiven,  UsedValue,Err,NumTypes) {
  1038.     # If option takes a value...    [
  1039.     NumTypes = "*()#<>]"
  1040.     if (Option == "&" && ArgType !~ "[" NumTypes) {    # ]
  1041.     OptErr = "Bad type given for & option"
  1042.     return -3
  1043.     }
  1044.  
  1045.     if (UsedValue = (ArgType ~ "[:;" NumTypes)) {    # ]
  1046.     if (!GotValue) {
  1047.         if (Name != "")
  1048.         OptErr = "Variable requires a value -- " Name
  1049.         else
  1050.         OptErr = "option requires an argument -- " Option
  1051.         return -1
  1052.     }
  1053.     if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") {
  1054.         OptErr = Err
  1055.         return -2
  1056.     }
  1057.     # Mark this as a numeric variable; will be propogated to Options[] val.
  1058.     if (ArgType != ":" && ArgType != ";")
  1059.         Value += 0
  1060.     if ((Instance = ++Options[Option,"count"]) > 1)
  1061.         Options[Option,Instance] = Value
  1062.     else
  1063.         Options[Option] = Value
  1064.     }
  1065.     # If this is an environ or rcfile assignment & it was given a value...
  1066.     else if (!OptionNum && Value != "") {
  1067.     UsedValue = 1
  1068.     # If the value is "0" or "-" and this is the first instance of it,
  1069.     # do not set Options[Option]; this allows an assignment in an rcfile to
  1070.     # turn off an option (for the simple "Option in Options" test) in such
  1071.     # a way that it cannot be turned on in a later file.
  1072.     if (!(Option in Options) && (Value == "0" || Value == "-"))
  1073.         Instance = 1
  1074.     else
  1075.         Instance = ++Options[Option]
  1076.     # Save the value even though this is a flag
  1077.     Options[Option,Instance] = Value
  1078.     }
  1079.     # If this is a command line flag and has a - following it in the same arg,
  1080.     # it is being turned off.
  1081.     else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") {
  1082.     UsedValue = 2
  1083.     if (Option in Options)
  1084.         Instance = ++Options[Option]
  1085.     else
  1086.         Instance = 1
  1087.     Options[Option,Instance]
  1088.     }
  1089.     # If this is a flag assignment without a value, increment the count for the
  1090.     # flag unless it was turned off.  The indicator for a flag being turned off
  1091.     # is that the flag index has not been set in Options[] but it has an
  1092.     # instance count.
  1093.     else if (Option in Options || !((Option,1) in Options))
  1094.     # Increment number of times this flag seen; will inc null value to 1
  1095.     Instance = ++Options[Option]
  1096.     Options[Option,"num",Instance] = OptionNum
  1097.     return UsedValue
  1098. }
  1099.  
  1100. # Option is the option letter
  1101. # Value is the value being assigned
  1102. # Name is the var name of the option, if any
  1103. # ArgType is one of:
  1104. # :    String argument
  1105. # ;    Non-null string argument
  1106. # *    Floating point argument
  1107. # (    Non-negative floating point argument
  1108. # )    Positive floating point argument
  1109. # #    Integer argument
  1110. # <    Non-negative integer argument
  1111. # >    Positive integer argument
  1112. # specGiven is the option specifier character use, if any (e.g. - or +),
  1113. # for use in error messages.
  1114. # Returns null on success, err string on error
  1115. function CheckType(ArgType,Value,Option,Name,specGiven,  Err,ErrStr) {
  1116.     if (ArgType == ":")
  1117.     return ""
  1118.     if (ArgType == ";") {
  1119.     if (Value == "")
  1120.         Err = "must be a non-empty string"
  1121.     }
  1122.     # A number begins with optional + or -, and is followed by a string of
  1123.     # digits or a decimal with digits before it, after it, or both
  1124.     else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/)
  1125.     Err = "must be a number"
  1126.     else if (ArgType ~ "[#<>]" && Value ~ /\./)
  1127.     Err = "may not include a fraction"
  1128.     else if (ArgType ~ "[()<>]" && Value < 0)
  1129.     Err = "may not be negative"
  1130.     # (
  1131.     else if (ArgType ~ "[)>]" && Value == 0)
  1132.     Err = "must be a positive number"
  1133.     if (Err != "") {
  1134.     ErrStr = "Bad value \"" Value "\".  Value assigned to "
  1135.     if (Name != "")
  1136.         return ErrStr "variable " substr(Name,1,1) " " Err
  1137.     else {
  1138.         if (Option == "&")
  1139.         Option = Value
  1140.         return ErrStr "option " specGiven substr(Option,1,1) " " Err
  1141.     }
  1142.     }
  1143.     else
  1144.     return ""
  1145. }
  1146.  
  1147. # Note: only the above functions are needed by ProcArgs.
  1148. # The rest of these functions call ProcArgs() and also do other
  1149. # option-processing stuff.
  1150.  
  1151. # Opts: Process command line arguments.
  1152. # Opts processes command line arguments using ProcArgs()
  1153. # and checks for errors.  If an error occurs, a message is printed
  1154. # and the program is exited.
  1155. #
  1156. # Input variables:
  1157. # Name is the name of the program, for error messages.
  1158. # Usage is a usage message, for error messages.
  1159. # OptList the option description string, as used by ProcArgs().
  1160. # MinArgs is the minimum number of non-option arguments that this
  1161. # program should have, non including ARGV[0] and +h.
  1162. # If the program does not require any non-option arguments,
  1163. # MinArgs should be omitted or given as 0.
  1164. # rcFiles, if given, is a colon-seprated list of filenames to read for
  1165. # variable initialization.  If a filename begins with ~/, the ~ is replaced
  1166. # by the value of the environment variable HOME.  If a filename begins with
  1167. # $, the part from the character after the $ up until (but not including)
  1168. # the first character not in [a-zA-Z0-9_] will be searched for in the
  1169. # environment; if found its value will be substituted, if not the filename will
  1170. # be discarded.
  1171. # rcfiles are read in the order given.
  1172. # Values given in them will not override values given on the command line,
  1173. # and values given in later files will not override those set in earlier
  1174. # files, because AssignVal() will store each with a different instance index.
  1175. # The first instance of each variable, either on the command line or in an
  1176. # rcfile, will be stored with no instance index, and this is the value
  1177. # normally used by programs that call this function.
  1178. # VarNames is a comma-separated list of variable names to map to options,
  1179. # in the same order as the options are given in OptList.
  1180. # If EnvSearch is given and nonzero, the first EnvSearch variables will also be
  1181. # searched for in the environment.  If set to -1, all values will be searched
  1182. # for in the environment.  Values given in the environment will override
  1183. # those given in the rcfiles but not those given on the command line.
  1184. # NoRCopt, if given, is an additional letter option that if given on the
  1185. # command line prevents the rcfiles from being read.
  1186. # See ProcArgs() for a description of AllowUnRecOpt and optChars, and
  1187. # ExclusiveOptions() for a description of exOpts.
  1188. # Special options:
  1189. # If x is made an option and is given, some debugging info is output.
  1190. # h is assumed to be the help option.
  1191.  
  1192. # Global variables:
  1193. # The command line arguments are taken from ARGV[].
  1194. # The arguments that are option specifiers and values are removed from
  1195. # ARGV[], leaving only ARGV[0] and the non-option arguments.
  1196. # The number of elements in ARGV[] should be in ARGC.
  1197. # After processing, ARGC is set to the number of elements left in ARGV[].
  1198. # The option values are put in Options[].
  1199. # On error, Err is set to a positive integer value so it can be checked for in
  1200. # an END block.
  1201. # Return value: The number of elements left in ARGV is returned.
  1202. # Must keep OptErr global since it may be set by InitOpts().
  1203. function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt,
  1204. AllowUnrecOpt,optChars,exOpts,  ArgsLeft,e) {
  1205.     if (MinArgs == "")
  1206.     MinArgs = 0
  1207.     ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt,
  1208.     optChars)
  1209.     if (ArgsLeft < (MinArgs+1) && !("h" in Options)) {
  1210.     if (ArgsLeft >= 0) {
  1211.         OptErr = "Not enough arguments"
  1212.         Err = 4
  1213.     }
  1214.     else
  1215.         Err = -ArgsLeft
  1216.     printf "%s: %s.\nUse -h for help.\n%s\n",
  1217.     Name,OptErr,Usage > "/dev/stderr"
  1218.     exit 1
  1219.     }
  1220.     if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) &&
  1221.     (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0)
  1222.     {
  1223.     print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr"
  1224.     Err = -e
  1225.     exit 1
  1226.     }
  1227.     if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != ""))
  1228.     {
  1229.     printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr"
  1230.     Err = 1
  1231.     exit 1
  1232.     }
  1233.     return ArgsLeft
  1234. }
  1235.  
  1236. # ReadConfFile(): Read a file containing var/value assignments, in the form
  1237. # <variable-name><assignment-char><value>.
  1238. # Whitespace (spaces and tabs) around a variable (leading whitespace on the
  1239. # line and whitespace between the variable name and the assignment character) 
  1240. # is stripped.  Lines that do not contain an assignment operator or which
  1241. # contain a null variable name are ignored, other than possibly being noted in
  1242. # the return value.  If more than one assignment is made to a variable, the
  1243. # first assignment is used.
  1244. # Input variables:
  1245. # File is the file to read.
  1246. # Comment is the line-comment character.  If it is found as the first non-
  1247. #     whitespace character on a line, the line is ignored.
  1248. # Assign is the assignment string.  The first instance of Assign on a line
  1249. #     separates the variable name from its value.
  1250. # If StripWhite is true, whitespace around the value (whitespace between the
  1251. #     assignment char and trailing whitespace on the line) is stripped.
  1252. # VarPat is a pattern that variable names must match.  
  1253. #     Example: "^[a-zA-Z][a-zA-Z0-9]+$"
  1254. # If FlagsOK is true, variables are allowed to be "set" by being put alone on
  1255. #     a line; no assignment operator is needed.  These variables are set in
  1256. #     the output array with a null value.  Lines containing nothing but
  1257. #     whitespace are still ignored.
  1258. # Output variables:
  1259. # Values[] contains the assignments, with the indexes being the variable names
  1260. #     and the values being the assigned values.
  1261. # Lines[] contains the line number that each variable occured on.  A flag set
  1262. #     is record by giving it an index in Lines[] but not in Values[].
  1263. # Return value:
  1264. # If any errors occur, a string consisting of descriptions of the errors
  1265. # separated by newlines is returned.  In no case will the string start with a
  1266. # numeric value.  If no errors occur,  the number of lines read is returned.
  1267. function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat,
  1268. FlagsOK,
  1269. Line,Status,Errs,AssignLen,LineNum,Var,Val) {
  1270.     if (Comment != "")
  1271.     Comment = "^" Comment
  1272.     AssignLen = length(Assign)
  1273.     if (VarPat == "")
  1274.     VarPat = "."    # null varname not allowed
  1275.     while ((Status = (getline Line < File)) == 1) {
  1276.     LineNum++
  1277.     sub("^[ \t]+","",Line)
  1278.     if (Line == "")        # blank line
  1279.         continue
  1280.     if (Comment != "" && Line ~ Comment)
  1281.         continue
  1282.     if (Pos = index(Line,Assign)) {
  1283.         Var = substr(Line,1,Pos-1)
  1284.         Val = substr(Line,Pos+AssignLen)
  1285.         if (StripWhite) {
  1286.         sub("^[ \t]+","",Val)
  1287.         sub("[ \t]+$","",Val)
  1288.         }
  1289.     }
  1290.     else {
  1291.         Var = Line    # If no value, var is entire line
  1292.         Val = ""
  1293.     }
  1294.     if (!FlagsOK && Val == "") {
  1295.         Errs = Errs \
  1296.         sprintf("\nBad assignment on line %d of file %s: %s",
  1297.         LineNum,File,Line)
  1298.         continue
  1299.     }
  1300.     sub("[ \t]+$","",Var)
  1301.     if (Var !~ VarPat) {
  1302.         Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s",
  1303.         LineNum,File,Var)
  1304.         continue
  1305.     }
  1306.     if (!(Var in Lines)) {
  1307.         Lines[Var] = LineNum
  1308.         if (Pos)
  1309.         Values[Var] = Val
  1310.     }
  1311.     }
  1312.     if (Status)
  1313.     Errs = Errs "\nCould not read file " File
  1314.     close(File)
  1315.     return Errs == "" ? LineNum : substr(Errs,2)    # Skip first newline
  1316. }
  1317.  
  1318. # Variables:
  1319. # Data is stored in Options[].
  1320. # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts().
  1321. # Global vars:
  1322. # Sets OptErr.  Uses ENVIRON[].
  1323. # If anything is read from any of the rcfiles, sets READ_RCFILE to 1.
  1324. function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch,
  1325. Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile,
  1326. fNames,numrcFiles,filesRead,Err,Values,retStr) {
  1327.     split("",filesRead,"")    # make awk know this is an array
  1328.     NumVars = split(VarNames,Vars,",")
  1329.     TypesInd = Ret = 0
  1330.     if (EnvSearch == -1)
  1331.     EnvSearch = NumVars
  1332.     for (i = 1; i <= NumVars; i++) {
  1333.     Var = Vars[i]
  1334.     CharOpt = substr(OptList,++TypesInd,1)
  1335.     if (CharOpt ~ "^[:;*()#<>&]$")
  1336.         CharOpt = substr(OptList,++TypesInd,1)
  1337.     Map[Var] = CharOpt
  1338.     Types[Var] = Type = substr(OptList,TypesInd+1,1)
  1339.     # Do not overwrite entries from environment
  1340.     if (i <= EnvSearch && Var in ENVIRON &&
  1341.     (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0)
  1342.         return Err
  1343.     }
  1344.  
  1345.     numrcFiles = split(rcFiles,fNames,":")
  1346.     for (i = 1; i <= numrcFiles; i++) {
  1347.     rcFile = fNames[i]
  1348.     if (rcFile ~ "^~/")
  1349.         rcFile = ENVIRON["HOME"] substr(rcFile,2)
  1350.     else if (rcFile ~ /^\$/) {
  1351.         rcFile = substr(rcFile,2)
  1352.         match(rcFile,"^[a-zA-Z0-9_]*")
  1353.         envvar = substr(rcFile,1,RLENGTH)
  1354.         if (envvar in ENVIRON)
  1355.         rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1)
  1356.         else
  1357.         continue
  1358.     }
  1359.     if (rcFile in filesRead)
  1360.         continue
  1361.     # rcfiles are liable to be given more than once, e.g. UHOME and HOME
  1362.     # may be the same
  1363.     filesRead[rcFile]
  1364.     if ("x" in Options)
  1365.         printf "Reading configuration file %s\n",rcFile > "/dev/stderr"
  1366.     retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1)
  1367.     if (retStr > 0)
  1368.         READ_RCFILE = 1
  1369.     else if (ret != "") {
  1370.         OptErr = retStr
  1371.         Ret = -1
  1372.     }
  1373.     for (Var in Lines)
  1374.         if (Var in Map) {
  1375.         if ((Err = AssignVal(Map[Var],
  1376.         Var in Values ? Values[Var] : "",Options,Types[Var],
  1377.         Var in Values,Var,0)) < 0)
  1378.             return Err
  1379.         }
  1380.         else {
  1381.         OptErr = sprintf(\
  1382.         "Unknown var \"%s\" assigned to on line %d\nof file %s",Var,
  1383.         Lines[Var],rcFile)
  1384.         Ret = -1
  1385.         }
  1386.     }
  1387.  
  1388.     if ("x" in Options)
  1389.     for (Var in Map)
  1390.         if (Map[Var] in Options)
  1391.         printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \
  1392.         "/dev/stderr"
  1393.         else
  1394.         printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr"
  1395.     return Ret
  1396. }
  1397.  
  1398. # OptSets is a semicolon-separated list of sets of option sets.
  1399. # Within a list of option sets, the option sets are separated by commas.  For
  1400. # each set of sets, if any option in one of the sets is in Options[] AND any
  1401. # option in one of the other sets is in Options[], an error string is returned.
  1402. # If no conflicts are found, nothing is returned.
  1403. # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to
  1404. # the exclusions presented by the first set of sets (ab,def,g) if:
  1405. # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR
  1406. # (a or b is in Options[]) AND (g is in Options) OR
  1407. # (d, e, or f is in Options[]) AND (g is in Options)
  1408. # An error will be returned due to the exclusions presented by the second set
  1409. # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]).
  1410. # todo: make options given on command line unset options given in config file
  1411. # todo: that they conflict with.
  1412. function ExclusiveOptions(OptSets,Options,
  1413. Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets,
  1414. SetNum,OSetNum) {
  1415.     NumSetSets = split(OptSets,SetSets,";")
  1416.     # For each set of sets...
  1417.     for (SetSet = 1; SetSet <= NumSetSets; SetSet++) {
  1418.     # NumSets is the number of sets in this set of sets.
  1419.     NumSets = split(SetSets[SetSet],Sets,",")
  1420.     # For each set in a set of sets except the last...
  1421.     for (SetNum = 1; SetNum < NumSets; SetNum++) {
  1422.         s1 = Sets[SetNum]
  1423.         L1 = length(s1)
  1424.         for (Pos1 = 1; Pos1 <= L1; Pos1++)
  1425.         # If any of the options in this set was given, check whether
  1426.         # any of the options in the other sets was given.  Only check
  1427.         # later sets since earlier sets will have already been checked
  1428.         # against this set.
  1429.         if ((c1 = substr(s1,Pos1,1)) in Options)
  1430.             for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) {
  1431.             s2 = Sets[OSetNum]
  1432.             L2 = length(s2)
  1433.             for (Pos2 = 1; Pos2 <= L2; Pos2++)
  1434.                 if ((c2 = substr(s2,Pos2,1)) in Options)
  1435.                 ErrStr = ErrStr "\n"\
  1436.                 sprintf("Cannot give both %s and %s options.",
  1437.                 c1,c2)
  1438.             }
  1439.     }
  1440.     }
  1441.     if (ErrStr != "")
  1442.     return substr(ErrStr,2)
  1443.     return ""
  1444. }
  1445.  
  1446. # The value of each instance of option Opt that occurs in Options[] is made an
  1447. # index of Set[].
  1448. # The return value is the number of instances of Opt in Options.
  1449. function Opt2Set(Options,Opt,Set,  count) {
  1450.     if (!(Opt in Options))
  1451.     return 0
  1452.     Set[Options[Opt]]
  1453.     count = Options[Opt,"count"]
  1454.     for (; count > 1; count--)
  1455.     Set[Options[Opt,count]]
  1456.     return count
  1457. }
  1458.  
  1459. # The value of each instance of option Opt that occurs in Options[] that
  1460. # begins with "!" is made an index of nSet[] (with the ! stripped from it).
  1461. # Other values are made indexes of Set[].
  1462. # The return value is the number of instances of Opt in Options.
  1463. function Opt2Sets(Options,Opt,Set,nSet,  count,aSet,ret) {
  1464.     ret = Opt2Set(Options,Opt,aSet)
  1465.     for (value in aSet)
  1466.     if (substr(value,1,1) == "!")
  1467.         nSet[substr(value,2)]
  1468.     else
  1469.         Set[value]
  1470.     return ret
  1471. }
  1472.  
  1473. # Returns true if option Opt was given on the command line.
  1474. function CmdLineOpt(Options,Opt,  i) {
  1475.     for (i = 1; (Opt,"num",i) in Options; i++)
  1476.     if (Options[Opt,"num",i] != 0)
  1477.         return 1
  1478.     return 0
  1479. }
  1480. ### End of ProcArgs library
  1481.